package baseFx.database;

import baseFx.common.Stream;
import baseFx.common.collections.StringKeyMap;
import baseFx.common.utils.*;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

public class DbEntity {
    private static Map<Class<?>, DbEntity> cache = new HashMap<>(64);
    private Class<?> clazz;
    private Stream<DbField> fields = Stream.create();
    private String style;
    private String tableName;
    private boolean allowDelete;

    public static DbEntity get(Class<?> clazz) {
        DbEntity entity = cache.getOrDefault(clazz, null);
        if (entity == null) {
            entity = new DbEntity(clazz);
            cache.put(clazz, entity);
        }
        return entity;
    }

    private DbEntity(Class<?> clazz) {
        this.clazz = clazz;
        loadTableName();
        loadFromMethod();
        loadFromField();
    }

    private void loadTableName() {
        Class<?> table = clazz;
        while (tableName == null && table != Object.class) {
            DbTable tabAttr = clazz.getAnnotation(DbTable.class);
            if (tabAttr != null) {
                if (StringUtils.hasValue(tabAttr.name())) {
                    tableName = tabAttr.name();
                } else if (StringUtils.hasValue(tabAttr.value())) {
                    tableName = tabAttr.value();
                }
                style = tabAttr.style();
                allowDelete = tabAttr.allowDelete();
            }
            if (tableName == null) {
                Resource resAttr = clazz.getAnnotation(Resource.class);
                if (resAttr != null) {
                    tableName = resAttr.name();
                    StringKeyMap<String> map = UrlUtils.toMap(resAttr.description());
                    style = map.getOrDefault("style", "");
                    allowDelete = "true".equalsIgnoreCase(map.getOrDefault("allowDelete", ""));
                }
            }
            if (tableName == null) {
                table = table.getSuperclass();
            }
        }
        if (tableName == null) {
            tableName = clazz.getSimpleName();
            style = "eq";
        }
    }

    public boolean isAllowDelete() {
        return allowDelete;
    }

    public String getTableName() {
        return tableName;
    }

    private String getColumnName(Member member) {
        if (member instanceof Field) {
            DbColumn cnAttr = ((Field) member).getAnnotation(DbColumn.class);
            if (cnAttr != null && StringUtils.hasValue(cnAttr.value())) {
                return cnAttr.value();
            }
            Resource resAttr = ((Field) member).getAnnotation(Resource.class);
            if (resAttr != null && StringUtils.hasValue(resAttr.name())) {
                return resAttr.name();
            }
        } else if (member instanceof Method) {
            DbColumn cnAttr = ((Method) member).getAnnotation(DbColumn.class);
            if (cnAttr != null && StringUtils.hasValue(cnAttr.value())) {
                return cnAttr.value();
            }
            Resource resAttr = ((Method) member).getAnnotation(Resource.class);
            if (resAttr != null && StringUtils.hasValue(resAttr.name())) {
                return resAttr.name();
            }
        }
        String name = member.getName();
        if ("".equals(style) || "eq".equals(style)) {
            return name;
        } else if ("_".equals(style)) {
            if (name.contains("_") == false) {
                StringBuilder sb = new StringBuilder();
                for (char c : name.toCharArray()) {
                    if (c >= 65 && c < 97) {
                        sb.append("_");
                        sb.append(String.valueOf(c).toLowerCase());
                    } else {
                        sb.append(c);
                    }
                }
                name = sb.toString();
            }
        } else {
            throw new RuntimeException("无法识别的style：" + style + ", 可选的值有：eq(java名等于数据库字段名),_(驼峰风格，用下划线隔开)");
        }
        return name;
    }

    private boolean getIsInc(Member member) {
        String desc = "";
        if (member instanceof Field) {
            DbColumn cnAttr = ((Field) member).getAnnotation(DbColumn.class);
            if (cnAttr != null) {
                return cnAttr.isInc();
            }
            Resource resAttr = ((Field) member).getAnnotation(Resource.class);
            if (resAttr != null) {
                desc = resAttr.description();
            }
        } else if (member instanceof Method) {
            DbColumn cnAttr = ((Method) member).getAnnotation(DbColumn.class);
            if (cnAttr != null) {
                return cnAttr.isInc();
            }
            Resource resAttr = ((Method) member).getAnnotation(Resource.class);
            if (resAttr != null) {
                desc = resAttr.description();
            }
        }
        if (StringUtils.hasValue(desc)) {
            StringKeyMap<String> map = UrlUtils.toMap(desc);
            String value = map.getOrDefault("isInc", "");
            return "1".equals(value) || "true".equals(value.toLowerCase());
        }
        return false;
    }

    private void loadFromMethod() {
        List<Method> msList = ClassUtils.getMethods(clazz);
        List<Method> bakList = new ArrayList<>(msList);
        for (Method set : msList) {
            if (fields.any(i -> i.javaName.equals(set.getName()))) {
                continue;
            }
            if (Modifier.isPublic(set.getModifiers()) == false) {
                continue;
            }
            if (Modifier.isStatic(set.getModifiers())) {
                continue;
            }
            if (set.getParameterCount() != 1) {
                continue;
            }
            if (set.getReturnType() != clazz && set.getReturnType() != void.class) {
                continue;
            }
            if (set.isAnnotationPresent(Ignore.class)) {
                continue;
            }
            Class<?> cls = set.getParameterTypes()[0];
            for (Method get : bakList) {
                if (Modifier.isPublic(get.getModifiers()) == false) {
                    continue;
                }
                if (Modifier.isStatic(get.getModifiers())) {
                    continue;
                }
                if (get.isAnnotationPresent(Ignore.class)) {
                    continue;
                }
                if (get.getName().equals(set.getName()) && get.getReturnType() == cls && get.getParameterCount() == 0) {
                    String column = getColumnName(get);
                    this.fields.add(new MethodSource(get.getName(), column, get, set, getIsInc(get) || getIsInc(set)));
                    break;
                }
            }
        }
    }

    private void loadFromField() {
        List<Field> fieldList = ClassUtils.fields(clazz);
        for (Field item : fieldList) {
            if (fields.any(i -> i.javaName.equals(item.getName()))) {
                continue;
            }
            if (Modifier.isPublic(item.getModifiers()) == false) {
                continue;
            }
            if (Modifier.isFinal(item.getModifiers())) {
                continue;
            }
            if (Modifier.isStatic(item.getModifiers())) {
                continue;
            }
            if (item.isAnnotationPresent(Ignore.class)) {
                continue;
            }
            String column = getColumnName(item);
            this.fields.add(new FieldSource(item.getName(), column, item, getIsInc(item)));
        }
    }

    public Map<String, Object> getValues(Object instance) {
        Map<String, Object> map = new LinkedHashMap<>(8);
        fields.foreach(item -> {
            Object value = item.get(instance);
            if (value != null) {
                if (item.type.isEnum()) {
                    Integer id = EnumUtils.getId((Enum<?>) value);
                    value = id != null ? id : ((Enum<?>) value).name();
                }
                map.put(item.dbName, value);
            }
        });
        return map;
    }

    public DbField getFieldByColumnName(String name) {
        return fields.first(i -> i.dbName.equalsIgnoreCase(name)).orElse(null);
    }

    public DbField getFieldByPropName(String name) {
        return fields.first(i -> i.javaName.equalsIgnoreCase(name)).orElse(null);
    }

    public List<DbField> getIncFields() {
        return fields.toList(i -> i.isInc);
    }

    public abstract static class DbField {
        public final String javaName;
        public final String dbName;
        public final Class<?> type;
        public final boolean isInc;

        protected DbField(String javaName, String dbName, Class<?> type, boolean isInc) {
            this.javaName = javaName;
            this.dbName = dbName;
            this.type = type;
            this.isInc = isInc;
        }

        public abstract Object get(Object obj);

        public abstract void set(Object obj, Object value);
    }

    private static class FieldSource extends DbField {
        private Field field;

        public FieldSource(String rawName, String dbName, Field field, boolean isInc) {
            super(rawName, dbName, field.getType(), isInc);
            this.field = field;
            if (field.isAccessible() == false) {
                field.setAccessible(true);
            }
        }

        @Override
        public Object get(Object obj) {
            try {
                return field.get(obj);
            } catch (IllegalAccessException e) {
                throw ExceptionUtils.toRuntime(e);
            }
        }

        @Override
        public void set(Object obj, Object value) {
            try {
                if (value != null && type.isEnum()) {
                    value = ConvertUtils.change(value, type);
                }
                field.set(obj, value);
            } catch (IllegalAccessException e) {
                throw ExceptionUtils.toRuntime(e);
            }
        }
    }

    private static class MethodSource extends DbField {
        private Method getter;
        private Method setter;

        public MethodSource(String rawName, String columnName, Method getter, Method setter, boolean isInc) {
            super(rawName, columnName, getter.getReturnType(), isInc);
            this.getter = getter;
            this.setter = setter;
            if (getter.isAccessible() == false) {
                getter.setAccessible(true);
            }
            if (setter.isAccessible() == false) {
                setter.setAccessible(true);
            }
        }

        @Override
        public Object get(Object obj) {
            try {
                return getter.invoke(obj);
            } catch (Exception e) {
                throw ExceptionUtils.toRuntime(e);
            }
        }

        @Override
        public void set(Object obj, Object value) {
            try {
                if (value != null && type.isEnum()) {
                    value = ConvertUtils.change(value, type);
                }
                setter.invoke(obj, value);
            } catch (Exception e) {
                throw ExceptionUtils.toRuntime(e);
            }
        }
    }
}
